import javax.swing.text.*;
import java.awt.*;
import java.util.HashSet;

/**
 Created by IntelliJ IDEA.
 User: Matt
 Date: Oct 21, 2010
 Time: 9:45:12 PM
 To change this template use File | Settings | File Templates. */
public class SyntaxDocument extends DefaultStyledDocument
{
    private HashSet<String> keywords;
    private DefaultStyledDocument doc;
    private Element rootElement;

    private boolean multiLineComment;
    private MutableAttributeSet normal;
    private MutableAttributeSet keyword;
    private MutableAttributeSet comment;
    private MutableAttributeSet quote;
    private MutableAttributeSet symbol;

    private TabSet tabSet;


    public SyntaxDocument() {
        //Set the defaultStyledDocument that we are going to be working with.
        doc = this;

        //The document is a DOM structure. So get the root node so we can work with that.
        rootElement = doc.getDefaultRootElement();

        //Set the end of line to be a new line.
        putProperty( DefaultEditorKit.EndOfLineStringProperty, "\n" );

        //Set a normal style where the color is black.
        normal = new SimpleAttributeSet();
        StyleConstants.setForeground( normal, Color.black );

        //Create a style for comments where they are grey and italicized.
        comment = new SimpleAttributeSet();
        StyleConstants.setForeground( comment, Color.gray );
        StyleConstants.setItalic( comment, true );

        //Create the style for keywords.
        keyword = new SimpleAttributeSet();
        StyleConstants.setForeground( keyword, Color.blue );

        //Create the styles for quotes.
        quote = new SimpleAttributeSet();
        StyleConstants.setForeground( quote, Color.red );

        //Create the styles for stymbols (:hey)
        symbol = new SimpleAttributeSet();
        StyleConstants.setForeground( symbol, Color.blue );

        //Sets the current text to the normal font. Gotta do this.
        setParagraphAttributes( 0, 0, normal, true );

        //Create a huge hash of all the ruby keywords.
        keywords = new HashSet<String>();
        keywords.add( "alias" );
        keywords.add( "and" );
        keywords.add( "begin" );
        keywords.add( "break" );
        keywords.add( "case" );
        keywords.add( "class" );
        keywords.add( "def" );
        keywords.add( "defined?" );
        keywords.add( "do" );
        keywords.add( "else" );
        keywords.add( "elsif" );
        keywords.add( "END" );
        keywords.add( "end" );
        keywords.add( "ensure" );
        keywords.add( "false" );
        keywords.add( "true" );
        keywords.add( "__FILE__" );
        keywords.add( "for" );
        keywords.add( "if" );
        keywords.add( "in" );
        keywords.add( "__LINE__" );
        keywords.add( "module" );
        keywords.add( "next" );
        keywords.add( "nil" );
        keywords.add( "not" );
        keywords.add( "or" );
        keywords.add( "redo" );
        keywords.add( "rescue" );
        keywords.add( "retry" );
        keywords.add( "return" );
        keywords.add( "self" );
        keywords.add( "super" );
        keywords.add( "then" );
        keywords.add( "true" );
        keywords.add( "undef" );
        keywords.add( "unless" );
        keywords.add( "until" );
        keywords.add( "when" );
        keywords.add( "while" );
        keywords.add( "yield" );


    }

    //We call this to remove characters from the document

    @Override
    public void remove( int offs, int len ) throws BadLocationException {
        //Remove the items.
        super.remove( offs, len );

        //Now that we have removed them go do syntax highlighting for this line again.
        processChangedLines( offs, len );
    }

    //We call this string to insert new text to the docuent. We also specify a attribute set for this (though
    //we really should be creating enums for this that we can pass in. This sucks to have to create your own or
    //have to deal with it that way.

    public void insertString( int offs, String str, AttributeSet a ) throws BadLocationException {
        //Replace all tabs with spaces. This is messed up but I couldn't get tabs to work
        //and didn't want to spend a ton of time trying to do that in the beginning.
        str = str.replaceAll( "\t", "    " );

        //Call the parent and insert our new tab free string.
        super.insertString( offs, str, a );

        //Go do syntax highlighting.
        processChangedLines( offs, str.length() );
    }

    //This is a helpful wrapper method to just allow us to pass in the position to insert text and then
    //the text itself and this will insert it with the default styling.

    public void insertDefaultText( int position, String text ) {
        try
        {
            insertString( position, text, normal );
        } catch( BadLocationException e )
        {
            e.printStackTrace();
        }
    }

    //This does the syntax highlighting. It is called when we make any changes to the document.

    public void processChangedLines( int offset, int length ) {
        try
        {
            //This does nothing but I might need it in the future so I'll leave it for now.
            String content = doc.getText( 0, doc.getLength() );

            //Get the first line number that was affected by the latest change.
            int startLineNum = rootElement.getElementIndex( offset );

            //Get the last line number that was affected by the latest change.
            int endLineNum = rootElement.getElementIndex( offset + length );

            //Iterate through all the lines from the first line changed to the last and do
            //syntax highlighting.
            for( int currentLineNum = startLineNum; currentLineNum <= endLineNum; currentLineNum++ )
            {
                //Get the position of the first character in the line we are highlighting.
                int startOfLine = rootElement.getElement( currentLineNum ).getStartOffset();

                //Get the position of the last character in the line we are highlighting.
                int endOfLine = rootElement.getElement( currentLineNum ).getEndOffset();

                //Create a line object from the beginning and ending positions we found.
                Line line = new Line( extractLine( startOfLine, endOfLine ) );

                //Get an iterator of each line.
                LineIterator li = line.iterator();

                //Reset our entire line's formatting. If we don't do this our stuff will
                //have odd already formatted areas there already. We don't want that.
                doc.setCharacterAttributes( startOfLine, endOfLine - startOfLine, normal, false );

                //This is the code that color codes comments. If it starts with a # then set the
                //whole line to the comment color. Or for some odd reason it's set to the keyword
                //color.
                if( line.getFirstChar().equals( "#" ) )
                {
                    doc.setCharacterAttributes( startOfLine, endOfLine - startOfLine, keyword, true );
                    return;
                }

                //Pass in the line iterator and see if there is a keyword there or not.
                checkForKeywordMatch( li, startOfLine );
            }


        } catch( BadLocationException e )
        {
            e.printStackTrace();  //To change body of catch statement use File content.length()| Settings | File Templates.
        }

    }

    //Checks a list of words to see if any of them are keywords or not and if so then it syntax highlights them

    private void checkForKeywordMatch( LineIterator li, int startIndex ) {
        //While we have more words in our line see if we have a token and if so set it's color to be a token color.
        while( li.hasNext() )
        {
            //Get the next word fromt he list
            String nextWord = li.next();

            //Turn the keyword into a line.
            Line line = new Line( nextWord );

            //See if it starts with a semicolon which means it's a symbol rather than a keyword.
            if( line.getFirstChar().equals( ":" ) )
            {
                //Set it to the apropriate color if is a symbol.
                doc.setCharacterAttributes( li.getKeywordStart() + startIndex, li.getKeywordEnd() - li.getKeywordStart(), symbol, true );

                //Skip the loop because this word is done with.
                continue;
            }

            //Cycle through all the language keywords and see if we have a match
            for( String keyword : keywords )
            {
                //Do we have a match? If so then change the color of that text within the syntaxDocument.
                if( nextWord.equals( keyword ) )
                {
                    //We have a match so set it the keyword color. Though right now it's swapped with quotes.
                    doc.setCharacterAttributes( li.getKeywordStart() + startIndex, li.getKeywordEnd() - li.getKeywordStart(), quote, true );
                }
            }
        }
    }

    //Get a line of text from the document based on a starting and ending position (i.e. the whole line).
    private String extractLine( int startIndex, int endIndex ) throws BadLocationException {
        return doc.getText( startIndex, endIndex - startIndex );
    }
}
